/*******************************************************************************
 * Copyright (c) 2014 Greg Marut.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 * 
 * Contributors:
 *    Greg Marut - initial API and implementation
 ******************************************************************************/
package com.gregmarut.util;

import java.util.Arrays;

/**
 * @author Greg Marut
 */
public class ByteArray
{
	// ** Primitives **//
	// holds the array of bytes
	protected byte[] aryBytes;
	
	/**
	 * The constructor for ByteArray
	 */
	public ByteArray()
	{
		// initialize the byte array
		aryBytes = new byte[0];
	}
	
	/**
	 * The constructor for ByteArray
	 * 
	 * @param intLength
	 */
	public ByteArray(final int intLength)
	{
		// initialize the byte array
		aryBytes = new byte[intLength];
		
		// fill the array with byte 0
		Arrays.fill(aryBytes, (byte) 0);
	}
	
	/**
	 * The constructor for ByteArray
	 * 
	 * @param aryBytes
	 */
	public ByteArray(final byte[] aryBytes)
	{
		// save the byte array
		this.aryBytes = aryBytes;
	}
	
	/**
	 * The constructor for ByteArray using a long value
	 * 
	 * @param lngValue
	 */
	public ByteArray(final long lngValue)
	{
		// create the array
		aryBytes = new byte[8];
		
		// for every byte
		for (int i = 0; i < 8; i++)
		{
			// assign the array of bytes
			aryBytes[i] = (byte) (lngValue >> ((7 - i) * 8));
		}
	}
	
	/**
	 * The constructor for ByteArray
	 * 
	 * @param aryBytes
	 * @param intLength
	 */
	public ByteArray(final byte[] aryBytes, int intLength)
	{
		// call the other constructor
		this(intLength);
		
		// determine the smaller value
		int intSmaller = Math.min(aryBytes.length, intLength);
		
		// for every byte
		for (int i = 0; i < intSmaller; i++)
		{
			this.aryBytes[i] = aryBytes[i];
		}
	}
	
	/**
	 * Appends a byte to the end of this byte array
	 * 
	 * @param byteAppend
	 */
	public void append(byte byteAppend)
	{
		append(new byte[]
		{
			byteAppend
		});
	}
	
	/**
	 * Appends an array of bytes to the end of this byte array
	 * 
	 * @param aryBytesToAppend
	 */
	public void append(byte[] aryBytesToAppend)
	{
		// create a new byte array object
		ByteArray baNew = combineWith(aryBytesToAppend);
		
		// reset the array of bytes
		aryBytes = baNew.getBytes();
	}
	
	/**
	 * Appends a boolean to the byte array
	 * @param blnToAppend
	 */
	public void append(boolean blnToAppend)
	{
		//determine the byte to append
		byte byteToAppend = (blnToAppend ? (byte) 1 : (byte) 0);
		
		//append the byte
		append(byteToAppend);
	}
	
	/**
	 * Combines two byte arrays
	 * 
	 * @param aryBytesToAppend
	 * @return
	 */
	public ByteArray combineWith(final byte[] aryBytesToAppend)
	{
		// holds the byte array to return
		byte[] byteReturn = new byte[aryBytes.length + aryBytesToAppend.length];
		
		// copy the first array
		for (int i = 0; i < aryBytes.length; i++)
		{
			byteReturn[i] = aryBytes[i];
		}
		
		// append the second array
		for (int i = 0; i < aryBytesToAppend.length; i++)
		{
			byteReturn[i + aryBytes.length] = aryBytesToAppend[i];
		}
		
		// return the new combined array
		return new ByteArray(byteReturn);
	}
	
	/**
	 * Copies a byte array with a specified length
	 * 
	 * @return
	 */
	public ByteArray copy()
	{
		return copy(aryBytes.length);
	}
	
	/**
	 * Copies a byte array with a specified length
	 * 
	 * @param intLength
	 * @return
	 */
	public ByteArray copy(int intLength)
	{
		// holds the byte array to return
		byte[] byteReturn = new byte[intLength];
		
		// fill the array with byte 0
		Arrays.fill(byteReturn, (byte) 0);
		
		// determine the smaller number
		int intSmallerLength = (aryBytes.length < intLength ? aryBytes.length
			: intLength);
		
		// loop until the smaller length
		for (int i = 0; i < intSmallerLength; i++)
		{
			byteReturn[i] = aryBytes[i];
		}
		
		return new ByteArray(byteReturn);
	}
	
	/**
	 * Extracts a sub array from a byte array
	 * 
	 * @param intIndex
	 * @param intLength
	 * @return
	 * @throws ArrayIndexOutOfBoundsException
	 */
	public ByteArray subArray(final int intIndex, final int intLength)
		throws ArrayIndexOutOfBoundsException
	{
		// holds the byte array to return
		byte[] byteReturn = new byte[intLength];
		
		// make sure the index is within the bounds
		if (intIndex < 0 || intIndex >= aryBytes.length)
		{
			throw new ArrayIndexOutOfBoundsException(
				"Index is not within the array bounds");
		}
		
		// make sure the length is not 0
		else if (intLength < 0)
		{
			throw new ArrayIndexOutOfBoundsException(
				"Length must be greater than 0");
		}
		
		// make sure the length does not exceed the byte array length
		else if (intIndex + intLength > aryBytes.length)
		{
			throw new ArrayIndexOutOfBoundsException(
				"Index + Length is not within the array bounds");
		}
		
		// loop through the array
		for (int i = intIndex; i < intIndex + intLength; i++)
		{
			// copy the byte from the array
			byteReturn[i - intIndex] = aryBytes[i];
		}
		
		// return the byte array
		return new ByteArray(byteReturn);
	}
	
	/**
	 * Extracts a sub array from a byte array
	 * 
	 * @param intIndex
	 * @return
	 * @throws ArrayIndexOutOfBoundsException
	 */
	public ByteArray subArray(final int intIndex)
		throws ArrayIndexOutOfBoundsException
	{
		// return the byte array
		return subArray(intIndex, aryBytes.length - intIndex);
	}
	
	/**
	 * Extracts a string from the bytes ignoring '0' bits
	 * 
	 * @return
	 */
	public String extractString()
	{
		// holds the string builder object to return
		StringBuilder sbReturn = new StringBuilder();
		
		// for every byte
		for (int i = 0; i < aryBytes.length; i++)
		{
			// extract the current character
			char chrCurrent = (char) (aryBytes[i] & 0xFF);
			
			// append the character
			if (chrCurrent > 0)
				sbReturn.append(chrCurrent);
		}
		
		// return the string
		return sbReturn.toString();
	}
	
	/**
	 * Resizes the current byte array
	 * 
	 * @param intNewSize
	 */
	public void resize(final int intNewSize)
	{
		// get the difference
		int intDifference = intNewSize - aryBytes.length;
		
		// check to see if the difference is positive
		if (intDifference > 0)
		{
			// add to the size of this byte array
			append(new byte[intDifference]);
		}
		// check to see if the difference is negative
		else if (intDifference < 0)
		{
			aryBytes = subArray(0, intNewSize).getBytes();
		}
	}
	
	/**
	 * Sets a value in the byte array at a specified index
	 * 
	 * @param intIndex
	 * @param byteValue
	 * @throws ArrayIndexOutOfBoundsException
	 */
	public void setByte(int intIndex, byte byteValue)
		throws ArrayIndexOutOfBoundsException
	{
		aryBytes[intIndex] = byteValue;
	}
	
	/**
	 * Returns a byte at a specified location
	 * 
	 * @param intIndex
	 * @return
	 * @throws ArrayIndexOutOfBoundsException
	 */
	public byte getByte(int intIndex) throws ArrayIndexOutOfBoundsException
	{
		return aryBytes[intIndex];
	}
	
	/**
	 * Sets a value in the byte array at a specified index
	 * 
	 * @param intIndex
	 * @param blnValue
	 * @throws ArrayIndexOutOfBoundsException
	 */
	public void setBoolean(int intIndex, boolean blnValue)
		throws ArrayIndexOutOfBoundsException
	{
		aryBytes[intIndex] = (byte) (blnValue ? 1 : 0);
	}
	
	/**
	 * Returns a bit at a specified location
	 * 
	 * @param intIndex
	 * @return
	 * @throws ArrayIndexOutOfBoundsException
	 */
	public boolean getBoolean(int intIndex)
		throws ArrayIndexOutOfBoundsException
	{
		return aryBytes[intIndex] > 0;
	}
	
	/**
	 * Returns a char at a specified location
	 * 
	 * @param intIndex
	 * @return
	 * @throws ArrayIndexOutOfBoundsException
	 */
	public char getChar(int intIndex) throws ArrayIndexOutOfBoundsException
	{
		return (char) aryBytes[intIndex];
	}
	
	@Override
	public int hashCode()
	{
		int hash = 7;
		hash = 17 * hash + Arrays.hashCode(this.aryBytes);
		return hash;
	}
	
	/**
	 * Returns an int at a specified location
	 * 
	 * @param intIndex
	 * @return
	 * @throws ArrayIndexOutOfBoundsException
	 */
	public int getInt(int intIndex) throws ArrayIndexOutOfBoundsException
	{
		return aryBytes[intIndex] & 0xFF;
	}
	
	/**
	 * Returns the array of bytes
	 * 
	 * @return
	 */
	public byte[] getBytes()
	{
		return aryBytes;
	}
	
	/**
	 * Returns the size of the byte array
	 * 
	 * @return
	 */
	public int getSize()
	{
		return aryBytes.length;
	}
	
	/**
	 * Override the toString method
	 */
	@Override
	public String toString()
	{
		// holds the string builder object to return
		StringBuilder sbReturn = new StringBuilder();
		
		// for every byte
		for (int i = 0; i < aryBytes.length; i++)
		{
			// extract the current character
			char chrCurrent = (char) (aryBytes[i] & 0xFF);
			
			// append the character
			sbReturn.append(chrCurrent);
		}
		
		// return the string
		return sbReturn.toString();
	}
	
	/**
	 * Returns the byte array as a hex string
	 * 
	 * @return
	 */
	public String toHexString()
	{
		// holds the string builder object to return
		StringBuilder sbReturn = new StringBuilder();
		
		// for every byte
		for (int i = 0; i < aryBytes.length; i++)
		{
			int intCurrent = aryBytes[i] & 0xFF;
			
			// check to see if a leading 0 is needed
			if (intCurrent < 0x10)
			{
				// append a 0
				sbReturn.append(0);
			}
			
			String s = Integer.toHexString(intCurrent).toUpperCase();
			
			// append the character
			sbReturn.append(s);
		}
		
		// return the string
		return sbReturn.toString();
	}
	
	/**
	 * Converts a byte array to an int value
	 * 
	 * @return
	 */
	public int toInt()
	{
		return (int) toLong();
	}
	
	/**
	 * Converts a byte array to a long value
	 * 
	 * @return
	 */
	public long toLong()
	{
		// holds the int to return
		long lngReturn = 0;
		
		// for every byte value
		for (int i = 0; i < aryBytes.length; i++)
		{
			// Extract the bits out of the array by "and"ing them with the
			// maximum value of
			// a byte. This is done because java does not support unsigned
			// types. Now that the
			// unsigned byte has been extracted, shift it to the right as far as
			// it is needed.
			// Examples:
			// byte array int
			// {0x01, 0x00} = 256
			//
			// byte array int
			// {0x01, 0x8C 0xF0} = 0x018CF0
			lngReturn += (byteToLong(aryBytes[i]) << (Byte.SIZE * (aryBytes.length
				- i - 1)));
		}
		
		// return the int
		return lngReturn;
	}
	
	/**
	 * Converts a byte to a long
	 * 
	 * @param b
	 * @return
	 */
	private long byteToLong(byte b)
	{
		return (b & 0xFF);
	}
	
	/**
	 * Override the equals
	 * 
	 * @param objTest
	 */
	@Override
	public boolean equals(final Object objTest)
	{
		// holds the result to return
		boolean blnResult = false;
		
		// check to see if this is an instance of byte array
		if (objTest instanceof ByteArray)
		{
			// recast the array
			ByteArray baTest = (ByteArray) objTest;
			
			// check to see if the arrays are the same
			blnResult = Arrays.equals(aryBytes, baTest.getBytes());
		}
		
		// return the result
		return blnResult;
	}
}
